// Copyright (c) 2011-2012 SameGoal LLC.
// All Rights Reserved.
// Author: Andy Hochhaus <ahochhaus@samegoal.com>

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "net/http/server/comet/comet.h"

#include <fcntl.h>
#include <gflags/gflags.h>
#include <glog/logging.h>
#include <openssl/err.h>
#include <openssl/rand.h>
#include <openssl/ssl.h>
#include <re2/re2.h>

#include <map>
#include <string>
#include <vector>

#include "base/init.h"
#include "base/string.h"
#include "net/http/server/comet/json.h"
#include "net/http/server/comet/proxy_detect_manager.h"
#include "net/http/server/comet/request_helper.h"
#include "net/http/server/comet/session.h"
#include "net/http/server/comet/session_manager.h"
#include "net/http/server/comet/timer_wrapper.h"

namespace sg {
namespace net {
namespace http {
namespace server {
namespace comet {

CometServer::CometServer(const std::string &ip,
                         const std::string &hostname,
                         const std::string &port,
                         const uint32 app_callback_pool_size)
    : EventServer(ip, hostname, port, app_callback_pool_size),
      host_prefix_("b"), blocked_prefix_("chatenabled"),
      test_handler_(&TestHandler, this), bind_handler_(&BindHandler, this),
      cleardot_handler_(&CleardotHandler, NULL),
      redirect_handler_(&RedirectHandler, this),
      channel_handler_(NULL),
      create_session_handler_(NULL), close_session_handler_(NULL) {
  TimerWrapper::Init(this->event_base());
}

void CometServer::SetAccessControlAllowOrigin(const std::string &origin) {
  origin_ = origin;
}

void CometServer::SetChannelHandler(ChannelCallback callback) {
  CHECK(channel_handler_ == NULL);
  channel_handler_ = callback;
}

void CometServer::SetCreateSessionHandler(CreateSessionCallback callback) {
  CHECK(create_session_handler_ == NULL);
  create_session_handler_ = callback;
}

void CometServer::SetCloseSessionHandler(CloseSessionCallback callback) {
  CHECK(close_session_handler_ == NULL);
  close_session_handler_ = callback;
}

void CometServer::SetHostPrefix(const std::string &host_prefix) {
  host_prefix_ = host_prefix;
}

void CometServer::SetBlockedPrefix(const std::string &blocked_prefix) {
  blocked_prefix_ = blocked_prefix;
}

bool CometServer::Send(const std::string &sid, const std::string &message) {
  SessionManager::SessionRefCntPtr session = SessionManager::GetSession(sid);
  if (!session) {
    LOG(WARNING) << "Send() to unknown sid: " << sid;
    return false;
  }
  std::vector<std::string> write_vec;
  write_vec.push_back(message);
  session->Write(write_vec);
  return true;
}

void CometServer::CloseSession(const std::string &sid) {
  if (close_session_handler_) {
    close_session_handler_(sid);
  }
}

void CometServer::TestHandler(es::Request *req, void *arg) {
  RequestHelper req_helper(req);

  CometServer *comet_server = static_cast<CometServer *>(arg);
  req->AddDefaultResponseHeaders();
  req->AddNonCacheableResponseHeaders();
  std::string type;
  req->GetQueryParam("TYPE", &type);
  if (type == "html") {
    req->AddResponseHeader("Content-Type", "text/html; charset=utf-8");
  } else {
    req->AddResponseHeader("Content-Type", "text/plain; charset=utf-8");
  }

  std::string mode;
  req->GetQueryParam("MODE", &mode);

  if (mode == "init") {
    // phase 1: ["b","chatenabled"]
    JsonArray phase1_resp;
    if (!comet_server->host_prefix_.empty()) {
      phase1_resp.AppendString(comet_server->host_prefix_);  // hostPrefix_
    } else {
      phase1_resp.AppendBytes("null");  // hostPrefix_
    }
    if (!comet_server->blocked_prefix_.empty()) {
      // blockedPrefix_
      phase1_resp.AppendString(comet_server->blocked_prefix_);
    } else {
      phase1_resp.AppendBytes("null");  // blockedPrefix_
    }
    req_helper.Send(es::OK, phase1_resp.Serialize(), RequestHelper::NONE);
  } else {
    // stage 2
    // multi-chunked response
    // content '11111'. wait two seconds. content '2'.
    if (!comet_server->origin_.empty()) {
      req->AddResponseHeader("Access-Control-Allow-Origin",
                             comet_server->origin_);
    }
    ProxyDetectManager::New(req);
  }
}

void CometServer::BindHandler(es::Request *req, void *arg) {
  RequestHelper req_helper(req);

  std::string type;
  req->GetQueryParam("TYPE", &type);
  std::string sid;
  req->GetQueryParam("SID", &sid);

  req->AddDefaultResponseHeaders();
  req->AddNonCacheableResponseHeaders();
  if (type == "html") {
    req->AddResponseHeader("Content-Type", "text/html; charset=utf-8");
  } else {
    req->AddResponseHeader("Content-Type", "text/plain; charset=utf-8");
  }

  CometServer *comet_server = static_cast<CometServer *>(arg);
  if (!comet_server->origin_.empty()) {
    req->AddResponseHeader("Access-Control-Allow-Origin",
                           comet_server->origin_);
  }

  if (type == "xmlhttp" || type == "html") {
    SessionManager::SessionRefCntPtr session = SessionManager::GetSession(sid);
    if (!session) {
      LOG(WARNING) << "No session to bind for sid: " << sid;
      req->SendError(es::INTERNAL_SERVER_ERROR, "Cannot find session.");
      return;
    }
    session->SetBackChannelRequest(req);
  } else if (type == "terminate") {
    SessionManager::Remove(sid);
    comet_server->CloseSession(sid);
    req_helper.Send(es::OK, "", RequestHelper::NONE);
    return;
  } else {
    bool new_sid = false;
    if (sid.empty()) {
      auto session = SessionManager::New(comet_server);
      sid = session->GetSid();
      new_sid = true;

      JsonArray session_info;
      // c = create session
      session_info.AppendString("c");
      // sessionId
      session_info.AppendString(session->GetSid());
      // hostPrefix_
      if (!comet_server->host_prefix_.empty()) {
        session_info.AppendString(comet_server->host_prefix_);  // hostPrefix_
      } else {
        session_info.AppendBytes("null");  // hostPrefix_
      }
      // negotiatedVersion
      session_info.AppendNumber(8);

      std::vector<std::string> write_vec;
      write_vec.push_back(session_info.Serialize());
      session->Write(write_vec);

      if (comet_server->create_session_handler_) {
        comet_server->create_session_handler_(sid, req);
      }
    }

    int64 count;
    if (req->GetPostAsInt64("count", &count) && count > 0) {
      int64 offset;
      req->GetPostAsInt64("ofs", &offset);

      es::Request::OrderedKeyValuePairs post = req->GetPostQueue();
      es::Request::OrderedKeyValuePairs::iterator post_iter = post.begin();
      // burn the first two params (count and ofs)
      if (post.size() < 2) {
        LOG(WARNING) << "post has too few items: " << post.size();
        return;
      }
      ++post_iter;
      ++post_iter;

      // TODO(ahochhaus): ignore any maps we have already received

      for (int64 i = 0; i < count; ++i) {
        // TODO(ahochhaus): use effective_map_id
        // int64 effective_map_id = offset + i;
        const std::string key_prefix = "req" + sg::string::ToString(i) + "_";

        std::map<std::string, std::string> incoming_map;
        while (post_iter != post.end() &&
               sg::string::StartsWith((*post_iter)->first, key_prefix)) {
          incoming_map[(*post_iter)->first.substr(key_prefix.length())] =
              (*post_iter)->second;
          ++post_iter;
        }
        const bool can_continue = comet_server->channel_handler_(
            sid, incoming_map, req);
        if (!can_continue) {
          // A fatal error was encountered while processing the forward channel
          // maps and a fatal error has been returned to the client. The
          // underlying req (and req_helper) have been closed. It is possible
          // the the underlying TCP connection has also been closed. Terminate,
          // to avoid future accesses to the request or the connection.
          return;
        }
      }
    }

    if (!new_sid) {
      SessionManager::SessionRefCntPtr session =
          SessionManager::GetSession(sid);
      if (!session) {
        LOG(WARNING) << "unknown sid: " << sid << " returning 404.";
        req_helper.Send(es::NOT_FOUND, "Unknown sid.", RequestHelper::NONE);
        return;
      }

      JsonArray back_info;
      // backchannelPresent
      back_info.AppendNumber(session->back_channel_present());
      // lastPostResponseArrayId_
      back_info.AppendNumber(session->last_array_id());
      // numOutstandingBackchannelBytes
      back_info.AppendNumber(session->outstanding_back_channel_bytes());

      req_helper.Send(
          es::OK,
          back_info.Serialize(),
          type == "html" ? RequestHelper::SCRIPT : RequestHelper::LENGTH);
    } else {
      SessionManager::SessionRefCntPtr session =
          SessionManager::GetSession(sid);
      CHECK(session);

      // No back channel, so we must flush to forward channel.
      session->FlushBuffer(req);
    }
  }
}

// Embed the cleardot *.gif file directly into the comet server binary to
// eliminate the external dependency. This was generated with:
// ./tools/bin_util.py
//     --bin_file=closure-library/closure/goog/images/tree/cleardot.gif
//     --output_file=/tmp/test.cc
const char cleardot_gif_ary[] = {
  static_cast<char>(0x47), static_cast<char>(0x49), static_cast<char>(0x46),
  static_cast<char>(0x38), static_cast<char>(0x39), static_cast<char>(0x61),
  static_cast<char>(0x01), static_cast<char>(0x00), static_cast<char>(0x01),
  static_cast<char>(0x00), static_cast<char>(0x80), static_cast<char>(0x00),
  static_cast<char>(0x00), static_cast<char>(0xff), static_cast<char>(0xff),
  static_cast<char>(0xff), static_cast<char>(0x00), static_cast<char>(0x00),
  static_cast<char>(0x00), static_cast<char>(0x21), static_cast<char>(0xf9),
  static_cast<char>(0x04), static_cast<char>(0x01), static_cast<char>(0x00),
  static_cast<char>(0x00), static_cast<char>(0x00), static_cast<char>(0x00),
  static_cast<char>(0x2c), static_cast<char>(0x00), static_cast<char>(0x00),
  static_cast<char>(0x00), static_cast<char>(0x00), static_cast<char>(0x01),
  static_cast<char>(0x00), static_cast<char>(0x01), static_cast<char>(0x00),
  static_cast<char>(0x00), static_cast<char>(0x02), static_cast<char>(0x02),
  static_cast<char>(0x44), static_cast<char>(0x01), static_cast<char>(0x00),
  static_cast<char>(0x3b)
};
const unsigned int cleardot_gif_len = 43;

const std::string redirect_start = "<!DOCTYPE html>\n"
    "<html lang=\"en\">\n"
    "  <head>\n"
    "    <title>Moved Permanently</title>\n"
    "    <meta charset=\"utf-8\">\n"
    "  </head>\n"
    "  <body>\n"
    "    <h1>Moved Permanently</h1>\n"
    "    The requested URL has moved permanently to <a href=\"";

const std::string redirect_middle = "\">";

const std::string redirect_end = "</a>.\n"
    "  </body>\n"
    "</html>\n";

void CometServer::CleardotHandler(es::Request *req, void */* arg */) {
  req->AddDefaultResponseHeaders();
  req->AddNonCacheableResponseHeaders();
  req->AddResponseHeader("Content-Type", "image/gif");
  RequestHelper req_helper(req);
  CHECK(req_helper.Send(es::OK,
                        std::string(cleardot_gif_ary, cleardot_gif_len),
                        RequestHelper::NONE));
}

void CometServer::RedirectHandler(es::Request *req, void *arg) {
  CometServer *comet_server = static_cast<CometServer *>(arg);
  req->AddDefaultResponseHeaders();
  req->AddNonCacheableResponseHeaders();

  req->AddResponseHeader("Content-Type", "text/html; charset=utf-8");

  const std::string protocol = comet_server->sctx() ? "https://" : "http://";
  const std::string default_port = comet_server->sctx() ? "443" : "80";

  std::string location_uri = protocol + comet_server->hostname();
  if (comet_server->port() != default_port) {
    location_uri += ":" + comet_server->port();
  }
  location_uri += req->request_path();
  req->AddResponseHeader("Location", location_uri);

  std::string response_body = "";
  req->Send(es::FOUND, response_body);
}

void CometServer::ConnectionCloseHandler(es::Request *req, void */* arg */) {
  std::string sid;
  req->GetQueryParam("SID", &sid);
  SessionManager::SessionRefCntPtr session = SessionManager::GetSession(sid);
  if (!session) {
    return;
  }
  session->OnBackChannelDisconnect();
  return;
}

void CometServer::InstallCometHandlers() {
  test_handler_.set_uri_regex("/channel/test", NULL);
  AddHandler(&test_handler_);
  bind_handler_.set_uri_regex("/channel/bind", NULL);
  AddHandler(&bind_handler_);
  cleardot_handler_.set_uri_regex(
      "/mail/images/cleardot.gif", NULL);
  AddHandler(&cleardot_handler_);
  SetConnectionCloseHandler(
      &ConnectionCloseHandler);
  // TODO(xzhang): enable SSL related redirects
  //
  // test_handler_.set_uri_regex("/channel/test",
  //                             {a.samegoal.com,
  //                             b.samegoal.com
  //                             c.samegoal.com,
  //                             samegoal.com});
  // AddHandler(&test_handler_);
  // bind_handler_.set_uri_regex("/channel/bind",
  //                             {a.samegoal.com,
  //                             b.samegoal.com,
  //                             c.samegoal.com,
  //                             samegoal.com});
  // AddHandler(&bind_handler_);
  // cleardot_handler_.set_uri_regex("/mail/images/cleardot.gif",
  //                                 {a.samegoal.com,
  //                                 b.samegoal.com,
  //                                 c.samegoal.com,
  //                                 samegoal.com});
  // AddHandler(&cleardot_handler_);
  redirect_handler_.set_not_protocol_host(hostname(), sctx() != NULL);
  AddHandler(&redirect_handler_);
}


}  // namespace comet
}  // namespace server
}  // namespace http
}  // namespace net
}  // namespace sg
